package org.apache.ibatis.builder.xml;

import org.apache.ibatis.builder.*;
import org.apache.ibatis.cache.Cache;
import org.apache.ibatis.executor.ErrorContext;
import org.apache.ibatis.io.Resources;
import org.apache.ibatis.mapping.*;
import org.apache.ibatis.parsing.XNode;
import org.apache.ibatis.parsing.XPathParser;
import org.apache.ibatis.session.Configuration;
import org.apache.ibatis.type.JdbcType;
import org.apache.ibatis.type.TypeHandler;

import java.io.InputStream;
import java.io.Reader;
import java.util.*;

/**
 * 负责解析映射配置文件，它继承了BaseBuilder抽象类，也是具体建造者的角色
 *
 * @author Clinton Begin
 */
public class XMLMapperBuilder extends BaseBuilder {

    /**
     * XPath解析对象
     */
    private final XPathParser parser;
    /** */
    private final MapperBuilderAssistant builderAssistant;
    /** */
    private final Map<String, XNode> sqlFragments;
    /**
     * mapper映射文件
     */
    private final String resource;

    @Deprecated
    public XMLMapperBuilder(Reader reader, Configuration configuration, String resource, Map<String, XNode> sqlFragments, String namespace) {
        this(reader, configuration, resource, sqlFragments);
        this.builderAssistant.setCurrentNamespace(namespace);
    }

    @Deprecated
    public XMLMapperBuilder(Reader reader, Configuration configuration, String resource, Map<String, XNode> sqlFragments) {
        this(new XPathParser(reader, true, configuration.getVariables(), new XMLMapperEntityResolver()),
                configuration, resource, sqlFragments);
    }

    public XMLMapperBuilder(InputStream inputStream, Configuration configuration, String resource, Map<String, XNode> sqlFragments, String namespace) {
        this(inputStream, configuration, resource, sqlFragments);
        this.builderAssistant.setCurrentNamespace(namespace);
    }

    public XMLMapperBuilder(InputStream inputStream, Configuration configuration, String resource, Map<String, XNode> sqlFragments) {
        this(new XPathParser(inputStream, true, configuration.getVariables(), new XMLMapperEntityResolver()),
                configuration, resource, sqlFragments);
    }

    private XMLMapperBuilder(XPathParser parser, Configuration configuration, String resource, Map<String, XNode> sqlFragments) {
        super(configuration);
        this.builderAssistant = new MapperBuilderAssistant(configuration, resource);
        this.parser = parser;
        this.sqlFragments = sqlFragments;
        this.resource = resource;
    }

    /**
     * 解析xml映射文件的入口
     * 1.解析 mapper 节点
     * 2.通过命名空间绑定 Mapper 接口
     * 3.处理未完成解析的节点
     */
    public void parse() {
        // 判断是否已经加载过该映射文件
        if (!configuration.isResourceLoaded(resource)) {
            // "/"表示从根节点查找
            // 处理<mapper>节点
            configurationElement(parser.evalNode("/mapper"));
            // 添加资源路径到“已解析资源集合”中
            // 将resource添加到Configuration.loadedResources集合中保存，
            // 它是HashSet<String>类型的集合，其中记录了已经加载过的映射文件。
            configuration.addLoadedResource(resource);
            // 注册Mapper接口，完成了映射配置文件与对应Mapper接口的绑定
            bindMapperForNamespace();
        }

        // 处理未完成解析的节点
        // 处理configurationElement()方法中解析失败的<resultMap>节点
        parsePendingResultMaps();
        // 处理configurationElement()方法中解析失败的<cache-ref>节点
        parsePendingCacheRefs();
        // 处理configurationElement()方法中解析失败的SQL语句节点
        parsePendingStatements();
    }

    public XNode getSqlFragment(String refid) {
        return sqlFragments.get(refid);
    }

    /**
     * 解析Mapper映射文件
     *
     * @param context
     */
    private void configurationElement(XNode context) {
        try {
            String namespace = context.getStringAttribute("namespace");
            if (namespace == null || namespace.equals("")) {
                // 如果namespace属性为空，则抛出异常
                throw new BuilderException("Mapper's namespace cannot be empty");
            }
            // 记录当前命名空间
            builderAssistant.setCurrentNamespace(namespace);
            // 解析<cache-ref>节点
            cacheRefElement(context.evalNode("cache-ref"));
            // 解析<cache>节点
            cacheElement(context.evalNode("cache"));
            // 解析<parameterMap>节点 (该节点已废弃，不再推荐使用，不做详细介绍)
            parameterMapElement(context.evalNodes("/mapper/parameterMap"));
            // 解析<resultMap>节点
            resultMapElements(context.evalNodes("/mapper/resultMap"));
            // 解析<sql>节点
            sqlElement(context.evalNodes("/mapper/sql"));
            // 解析<select>、<insert>、<update>、<delete>等SQL节点
            buildStatementFromContext(context.evalNodes("select|insert|update|delete"));
        } catch (Exception e) {
            throw new BuilderException("Error parsing Mapper XML. The XML location is '" + resource + "'. Cause: " + e, e);
        }
    }

    private void buildStatementFromContext(List<XNode> list) {
        if (configuration.getDatabaseId() != null) {
            // 调用重载方法构建 Statement
            buildStatementFromContext(list, configuration.getDatabaseId());
        }
        // 调用重载方法构建 Statement，requiredDatabaseId 参数为空
        buildStatementFromContext(list, null);
    }

    private void buildStatementFromContext(List<XNode> list, String requiredDatabaseId) {
        for (XNode context : list) {
            // 创建 Statement 建造类
            final XMLStatementBuilder statementParser = new XMLStatementBuilder(configuration, builderAssistant, context, requiredDatabaseId);
            try {
                /*
                 * 解析 Statement 节点，并将解析结果存储到
                 * configuration 的 mappedStatements 集合中
                 */
                statementParser.parseStatementNode();
            } catch (IncompleteElementException e) {
                // 解析失败，将解析器放入 configuration 的 incompleteStatements 集合中
                configuration.addIncompleteStatement(statementParser);
            }
        }
    }

    private void parsePendingResultMaps() {
        Collection<ResultMapResolver> incompleteResultMaps = configuration.getIncompleteResultMaps();
        synchronized (incompleteResultMaps) {
            Iterator<ResultMapResolver> iter = incompleteResultMaps.iterator();
            while (iter.hasNext()) {
                try {
                    iter.next().resolve();
                    iter.remove();
                } catch (IncompleteElementException e) {
                    // ResultMap is still missing a resource...
                }
            }
        }
    }

    private void parsePendingCacheRefs() {
        // 获取 CacheRefResolver 列表
        Collection<CacheRefResolver> incompleteCacheRefs = configuration.getIncompleteCacheRefs();
        synchronized (incompleteCacheRefs) {
            Iterator<CacheRefResolver> iter = incompleteCacheRefs.iterator();
            // 通过迭代器遍历列表
            while (iter.hasNext()) {
                try {
                    /*
                     * 尝试解析 <cache-ref> 节点，若解析失败，则抛出 IncompleteElementException，
                     * 此时下面的删除操作不会被执行
                     */
                    iter.next().resolveCacheRef();
                    /*
                     * 移除 CacheRefResolver 对象。如果代码能执行到此处，
                     * 表明已成功解析了 <cache-ref> 节点
                     */
                    iter.remove();
                } catch (IncompleteElementException e) {
                    // Cache ref is still missing a resource...
                    /*
                     * 如果再次发生 IncompleteElementException 异常，表明当前映射文件中并没有
                     * <cache-ref> 所引用的缓存。有可能所引用的缓存在后面的映射文件中，所以这里
                     * 不能将解析失败的 CacheRefResolver 从集合中删除
                     */
                }
            }
        }
    }

    private void parsePendingStatements() {
        Collection<XMLStatementBuilder> incompleteStatements = configuration.getIncompleteStatements();
        synchronized (incompleteStatements) {
            Iterator<XMLStatementBuilder> iter = incompleteStatements.iterator();
            while (iter.hasNext()) {
                try {
                    iter.next().parseStatementNode();
                    iter.remove();
                } catch (IncompleteElementException e) {
                    // Statement is still missing a resource...
                }
            }
        }
    }

    /**
     * 在 MyBatis 中，二级缓存是可以共用的。这需要使用 <cache-ref> 节点配置参照缓存，比如像下面这样。
     * <!-- Mapper1.xml -->
     * <mapper namespace="xyz.coolblog.dao.Mapper1">
     * <!-- Mapper1 与 Mapper2 共用一个二级缓存 -->
     * <cache-ref namespace="xyz.coolblog.dao.Mapper2"/>
     * </mapper>
     * <p>
     * <!-- Mapper2.xml -->
     * <mapper namespace="xyz.coolblog.dao.Mapper2">
     * <cache/>
     * </mapper>
     */
    private void cacheRefElement(XNode context) {
        if (context != null) {
            // 将当前Mapper配置文件的namespace与被引用的Cache所在的namespace之间的对应关系，
            // 记录到Configuration.cacheRefMap集合中
            configuration.addCacheRef(builderAssistant.getCurrentNamespace(), context.getStringAttribute("namespace"));
            // 创建 CacheRefResolver 实例
            CacheRefResolver cacheRefResolver = new CacheRefResolver(builderAssistant, context.getStringAttribute("namespace"));
            try {
                // 解析参照缓存
                cacheRefResolver.resolveCacheRef();
            } catch (IncompleteElementException e) {
                /*
                 * 这里对 IncompleteElementException 异常进行捕捉，并将 cacheRefResolver
                 * 存入到 Configuration 的 incompleteCacheRefs 集合中
                 */
                configuration.addIncompleteCacheRef(cacheRefResolver);
            }
        }
    }

    private void cacheElement(XNode context) throws Exception {
        if (context != null) {
            // 判空操作，若用户未设置 cache 节点的 type 和 eviction 属性，这里设置默认值 PERPETUAL
            String type = context.getStringAttribute("type", "PERPETUAL");
            Class<? extends Cache> typeClass = typeAliasRegistry.resolveAlias(type);
            String eviction = context.getStringAttribute("eviction", "LRU");
            Class<? extends Cache> evictionClass = typeAliasRegistry.resolveAlias(eviction);
            Long flushInterval = context.getLongAttribute("flushInterval");
            Integer size = context.getIntAttribute("size");
            boolean readWrite = !context.getBooleanAttribute("readOnly", false);
            boolean blocking = context.getBooleanAttribute("blocking", false);
            // 获取<cache>节点下的子节点，将用于初始化二级缓存
            Properties props = context.getChildrenAsProperties();
            // 通过MapperBuilderAssistant创建Cache对象，并添加到Configuration.caches集合中保存
            builderAssistant.useNewCache(typeClass, evictionClass, flushInterval, size, readWrite, blocking, props);
        }
    }

    private void parameterMapElement(List<XNode> list) throws Exception {
        for (XNode parameterMapNode : list) {
            String id = parameterMapNode.getStringAttribute("id");
            String type = parameterMapNode.getStringAttribute("type");
            Class<?> parameterClass = resolveClass(type);
            List<XNode> parameterNodes = parameterMapNode.evalNodes("parameter");
            List<ParameterMapping> parameterMappings = new ArrayList<ParameterMapping>();
            for (XNode parameterNode : parameterNodes) {
                String property = parameterNode.getStringAttribute("property");
                String javaType = parameterNode.getStringAttribute("javaType");
                String jdbcType = parameterNode.getStringAttribute("jdbcType");
                String resultMap = parameterNode.getStringAttribute("resultMap");
                String mode = parameterNode.getStringAttribute("mode");
                String typeHandler = parameterNode.getStringAttribute("typeHandler");
                Integer numericScale = parameterNode.getIntAttribute("numericScale");
                ParameterMode modeEnum = resolveParameterMode(mode);
                Class<?> javaTypeClass = resolveClass(javaType);
                JdbcType jdbcTypeEnum = resolveJdbcType(jdbcType);
                @SuppressWarnings("unchecked")
                Class<? extends TypeHandler<?>> typeHandlerClass = (Class<? extends TypeHandler<?>>) resolveClass(typeHandler);
                ParameterMapping parameterMapping = builderAssistant.buildParameterMapping(parameterClass, property, javaTypeClass, jdbcTypeEnum, resultMap, modeEnum, typeHandlerClass, numericScale);
                parameterMappings.add(parameterMapping);
            }
            builderAssistant.addParameterMap(id, parameterClass, parameterMappings);
        }
    }

    /**
     * resultMap 元素是 MyBatis 中最重要最强大的元素，它可以把大家从 JDBC ResultSets 数据提取的工作中解放出来。
     * 通过 resultMap 和自动映射，可以让 MyBatis 帮助我们完成 ResultSet → Object 的映射，这将会大大提高了开发效率。
     */
    private void resultMapElements(List<XNode> list) throws Exception {
        // 遍历 <resultMap> 节点列表
        for (XNode resultMapNode : list) {
            try {
                // 解析 resultMap 节点
                resultMapElement(resultMapNode);
            } catch (IncompleteElementException e) {
                // ignore, it will be retried
            }
        }
    }

    private ResultMap resultMapElement(XNode resultMapNode) throws Exception {
        // 调用重载方法
        return resultMapElement(resultMapNode, Collections.<ResultMapping>emptyList());
    }

    /**
     * 1.获取 <resultMap> 节点的各种属性
     * 2.解析 resultMap 对应的类型
     * 3.遍历 <resultMap> 的子节点,并根据子节点名称执行相应的解析逻辑,构建 ResultMapping 对象
     * 3.构建 ResultMap 对象
     * 4.若构建过程中发生异常，则将 resultMapResolver 添加到 incompleteResultMaps 集合中
     */
    private ResultMap resultMapElement(XNode resultMapNode, List<ResultMapping> additionalResultMappings) throws Exception {
        ErrorContext.instance().activity("processing " + resultMapNode.getValueBasedIdentifier());
        // 获取 id 和 type 属性
        String id = resultMapNode.getStringAttribute("id",
                resultMapNode.getValueBasedIdentifier());
        String type = resultMapNode.getStringAttribute("type",
                resultMapNode.getStringAttribute("ofType",
                        resultMapNode.getStringAttribute("resultType",
                                resultMapNode.getStringAttribute("javaType"))));
        // 获取 extends 和 autoMapping
        String extend = resultMapNode.getStringAttribute("extends");
        Boolean autoMapping = resultMapNode.getBooleanAttribute("autoMapping");
        // 解析 type 属性对应的类型
        Class<?> typeClass = resolveClass(type);
        Discriminator discriminator = null;
        List<ResultMapping> resultMappings = new ArrayList<ResultMapping>();
        resultMappings.addAll(additionalResultMappings);
        // 获取并遍历 <resultMap> 的子节点列表
        List<XNode> resultChildren = resultMapNode.getChildren();
        for (XNode resultChild : resultChildren) {
            if ("constructor".equals(resultChild.getName())) {
                // 解析 constructor 节点，并生成相应的 ResultMapping
                processConstructorElement(resultChild, typeClass, resultMappings);
            } else if ("discriminator".equals(resultChild.getName())) {
                // 解析 discriminator 节点
                discriminator = processDiscriminatorElement(resultChild, typeClass, resultMappings);
            } else {
                List<ResultFlag> flags = new ArrayList<ResultFlag>();
                if ("id".equals(resultChild.getName())) {
                    // 添加 ID 到 flags 集合中
                    flags.add(ResultFlag.ID);
                }
                // 解析 id 和 property 节点，并生成相应的 ResultMapping
                resultMappings.add(buildResultMappingFromContext(resultChild, typeClass, flags));
            }
        }
        // 创建 ResultMap 解析器
        ResultMapResolver resultMapResolver = new ResultMapResolver(builderAssistant, id, typeClass, extend, discriminator, resultMappings, autoMapping);
        try {
            // 根据前面获取到的信息构建 ResultMap 对象
            return resultMapResolver.resolve();
        } catch (IncompleteElementException e) {
            /*
             * 如果发生 IncompleteElementException 异常，
             * 这里将 resultMapResolver 添加到 incompleteResultMaps 集合中
             */
            configuration.addIncompleteResultMap(resultMapResolver);
            throw e;
        }
    }

    private void processConstructorElement(XNode resultChild, Class<?> resultType, List<ResultMapping> resultMappings) throws Exception {
        // 获取子节点列表
        List<XNode> argChildren = resultChild.getChildren();
        for (XNode argChild : argChildren) {
            List<ResultFlag> flags = new ArrayList<ResultFlag>();
            // 向 flags 中添加 CONSTRUCTOR 标志
            flags.add(ResultFlag.CONSTRUCTOR);
            if ("idArg".equals(argChild.getName())) {
                // 向 flags 中添加 ID 标志
                flags.add(ResultFlag.ID);
            }
            // 构建 ResultMapping，上一节已经分析过
            resultMappings.add(buildResultMappingFromContext(argChild, resultType, flags));
        }
    }

    private Discriminator processDiscriminatorElement(XNode context, Class<?> resultType, List<ResultMapping> resultMappings) throws Exception {
        String column = context.getStringAttribute("column");
        String javaType = context.getStringAttribute("javaType");
        String jdbcType = context.getStringAttribute("jdbcType");
        String typeHandler = context.getStringAttribute("typeHandler");
        Class<?> javaTypeClass = resolveClass(javaType);
        @SuppressWarnings("unchecked")
        Class<? extends TypeHandler<?>> typeHandlerClass = (Class<? extends TypeHandler<?>>) resolveClass(typeHandler);
        JdbcType jdbcTypeEnum = resolveJdbcType(jdbcType);
        Map<String, String> discriminatorMap = new HashMap<String, String>();
        for (XNode caseChild : context.getChildren()) {
            String value = caseChild.getStringAttribute("value");
            String resultMap = caseChild.getStringAttribute("resultMap", processNestedResultMappings(caseChild, resultMappings));
            discriminatorMap.put(value, resultMap);
        }
        return builderAssistant.buildDiscriminator(resultType, column, javaTypeClass, jdbcTypeEnum, typeHandlerClass, discriminatorMap);
    }

    /**
     * 这个方法需要大家注意一下，如果 Configuration 的 databaseId 不为空，sqlElement 方法会被调用了两次。
     * 第一次传入具体的 databaseId，用于解析带有 databaseId 属性，且属性值与此相等的 <sql> 节点。
     * 第二次传入的 databaseId 为空，用于解析未配置 databaseId 属性的 <sql> 节点。
     */
    private void sqlElement(List<XNode> list) throws Exception {
        if (configuration.getDatabaseId() != null) {
            // 调用 sqlElement 解析 <sql> 节点
            sqlElement(list, configuration.getDatabaseId());
        }
        // 再次调用 sqlElement，不同的是，这次调用，该方法的第二个参数为 null
        sqlElement(list, null);
    }

    private void sqlElement(List<XNode> list, String requiredDatabaseId) throws Exception {
        for (XNode context : list) {
            // 获取 id 和 databaseId 属性
            String databaseId = context.getStringAttribute("databaseId");
            String id = context.getStringAttribute("id");
            // 为id添加命名空间
            // id = currentNamespace + "." + id
            id = builderAssistant.applyCurrentNamespace(id, false);
            // 检测<sql>的databaseId与当前Configuration中记录的databaseId是否一致
            // 检测当前 databaseId 和 requiredDatabaseId 是否一致
            if (databaseIdMatchesCurrent(id, databaseId, requiredDatabaseId)) {
                // 记录到XMLMapperBuilder.sqlFragments(Map<String, XNode>类型)中保存，在
                // XMLMapperBuilder的构造函数中，可以看到该字段指向了Configuration.sqlFragments集合
                // 将 <id, XNode> 键值对缓存到 sqlFragments 中
                sqlFragments.put(id, context);
            }
        }
    }

    /**
     * databaseId 用于标明数据库厂商的身份，不同厂商有自己的 SQL 方言，MyBatis 可以根据 databaseId 执行不同 SQL 语句。
     * databaseId 在 <sql> 节点中有什么用呢？这个问题也不难回答。<sql> 节点用于保存 SQL 语句片段，
     * 如果 SQL 语句片段中包含方言的话，那么该 <sql> 节点只能被同一 databaseId 的查询语句或更新语句引用。
     */
    private boolean databaseIdMatchesCurrent(String id, String databaseId, String requiredDatabaseId) {
        if (requiredDatabaseId != null) {
            // 当前 databaseId 和目标 databaseId 不一致时，返回 false
            if (!requiredDatabaseId.equals(databaseId)) {
                return false;
            }
        } else {
            // 如果目标 databaseId 为空，但当前 databaseId 不为空。两者不一致，返回 false
            if (databaseId != null) {
                return false;
            }
            // skip this fragment if there is a previous one with a not null databaseId
            /*
             * 如果当前 <sql> 节点的 id 与之前的 <sql> 节点重复，且先前节点
             * databaseId 不为空。则忽略当前节点，并返回 false
             */
            if (this.sqlFragments.containsKey(id)) {
                XNode context = this.sqlFragments.get(id);
                if (context.getStringAttribute("databaseId") != null) {
                    return false;
                }
            }
        }
        return true;
    }

    private ResultMapping buildResultMappingFromContext(XNode context, Class<?> resultType, List<ResultFlag> flags) throws Exception {
        String property;
        // 根据节点类型获取 name 或 property 属性
        if (flags.contains(ResultFlag.CONSTRUCTOR)) {
            property = context.getStringAttribute("name");
        } else {
            property = context.getStringAttribute("property");
        }
        // 获取其他各种属性
        String column = context.getStringAttribute("column");
        String javaType = context.getStringAttribute("javaType");
        String jdbcType = context.getStringAttribute("jdbcType");
        String nestedSelect = context.getStringAttribute("select");
        /*
         * 嵌套结果处理
         * 解析 resultMap 属性，该属性出现在 <association> 和 <collection> 节点中。
         * 若这两个节点不包含 resultMap 属性，则调用 processNestedResultMappings 方法解析嵌套 resultMap。
         */
        String nestedResultMap = context.getStringAttribute("resultMap",
                processNestedResultMappings(context, Collections.<ResultMapping>emptyList()));
        String notNullColumn = context.getStringAttribute("notNullColumn");
        String columnPrefix = context.getStringAttribute("columnPrefix");
        String typeHandler = context.getStringAttribute("typeHandler");
        String resultSet = context.getStringAttribute("resultSet");
        String foreignColumn = context.getStringAttribute("foreignColumn");
        boolean lazy = "lazy".equals(context.getStringAttribute("fetchType", configuration.isLazyLoadingEnabled() ? "lazy" : "eager"));
        // 解析 javaType、typeHandler 的类型以及枚举类型 JdbcType
        Class<?> javaTypeClass = resolveClass(javaType);
        @SuppressWarnings("unchecked")
        Class<? extends TypeHandler<?>> typeHandlerClass = (Class<? extends TypeHandler<?>>) resolveClass(typeHandler);
        JdbcType jdbcTypeEnum = resolveJdbcType(jdbcType);
        // 构建 ResultMapping 对象
        return builderAssistant.buildResultMapping(resultType, property, column, javaTypeClass, jdbcTypeEnum, nestedSelect, nestedResultMap, notNullColumn, columnPrefix, typeHandlerClass, flags, resultSet, foreignColumn, lazy);
    }

    private String processNestedResultMappings(XNode context, List<ResultMapping> resultMappings) throws Exception {
        // 判断节点名称
        if ("association".equals(context.getName())
                || "collection".equals(context.getName())
                || "case".equals(context.getName())) {
            if (context.getStringAttribute("select") == null) {
                // resultMapElement 是解析 ResultMap 入口方法
                ResultMap resultMap = resultMapElement(context, resultMappings);
                return resultMap.getId();
            }
        }
        return null;
    }

    /**
     * 映射文件解析完成后，并不意味着整个解析过程就结束了。
     * 此时还需要通过命名空间绑定 mapper 接口，这样才能将映射文件中的 SQL 语句和 mapper 接口中的方法绑定在一起，
     * 后续即可通过调用 mapper 接口方法执行与之对应的 SQL 语句。
     * 下面我们来分析一下 mapper 接口的绑定过程。
     */
    private void bindMapperForNamespace() {
        // 获取映射文件的命名空间
        String namespace = builderAssistant.getCurrentNamespace();
        if (namespace != null) {
            Class<?> boundType = null;
            try {
                // 解析命名空间对应的类型
                // 根据命名空间解析 mapper 类型
                boundType = Resources.classForName(namespace);
            } catch (ClassNotFoundException e) {
                //ignore, bound type is not required
            }
            if (boundType != null) {
                // 检测当前 mapper 类是否被绑定过
                if (!configuration.hasMapper(boundType)) {
                    // Spring may not know the real resource name so we set a flag
                    // to prevent loading again this resource from the mapper interface
                    // look at MapperAnnotationBuilder#loadXmlResource
                    configuration.addLoadedResource("namespace:" + namespace);
                    // 注册boundType接口
                    // 绑定 mapper 类
                    configuration.addMapper(boundType);
                }
            }
        }
    }

}
